from django_simpleaggregation.models import Aggregate
from django_simpleaggregation.utils import smart_getattr
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import pre_save, post_save, post_delete
from django.dispatch import dispatcher
from django.utils.encoding import smart_unicode

class IncorrectRegistrationException(Exception):
    """
    This is thrown if something invalid is registered as an
    aggregate.
    """
    pass

class AggregationRegistry(object):
    """
    Registers models with the aggregation subsystem.
    Models can be registered multiple times, as long as a
    different field is chosen for uniqueness.
    """
    def __init__(self):
        self.registered_aggregates = []

    def register(self, *args):
        if not (args[0] and args[1]):
            raise IncorrectRegistrationException
        to_register = { 
            'content_type' : ContentType.objects.get_for_model(args[0]), 
            'unique_field' : args[1],
        }
        if len(args) == 3:
            to_register['callback'] = args[2]
        if not to_register in self.registered_aggregates:
            self.registered_aggregates.append(to_register)

    def __iter__(self):
        for aggregate in self.registered_aggregates:
            yield aggregate

# This is the default aggregation registry.  There is no reason
# why a subclass with specific functionality couldn't be instantiated
# elsewhere as well.
aggregates = AggregationRegistry()

def pre_save_annotation(sender, instance):
    """
    If the instance has no id, it means it has not yet been created.
    This is needed to differentiate whether an object is being created
    or being updated.
    
    If it is being created, the attribute '_should_increment' is
    annotated onto the current model instance.
    
    A check occurs later to make sure that the '_should_increment'
    attribute is really there.
    """
    # If it doesn't have an id, it's being created for the first time.
    if not hasattr(instance, 'id') or not getattr(instance, "id"):
        instance._should_increment = True
    else:
        # Get the saved object to compare with the current object
        previous = sender.objects.get(id=instance.id)
        content_type = ContentType.objects.get_for_model(sender)
        for aggregate in aggregates:
            if aggregate['content_type'] == content_type:
                unique_val = smart_getattr(instance, str(aggregate['unique_field']))
                prev_unique_val = smart_getattr(previous, str(aggregate['unique_field']))
                # If the unique field value has changed...
                if unique_val != prev_unique_val:
                    # ...decrement the old one
                    decrement_aggregate(sender, previous)
                    # ...and set the flag to increment the new one
                    instance._should_increment = True

# TODO: Un-violate DRY by combining increment_aggregate and 
# decrement_aggregate into one, larger, function.

def increment_aggregate(sender, instance):
    """
    Called post_save.  This will check for uniqueness and create a new
    Aggregate object if needed.  Also, if any callback functions are
    supplied, those will be evaluated here also.
    """
    if hasattr(instance, '_should_increment'):
        content_type = ContentType.objects.get_for_model(sender)
        for aggregate in aggregates:
            if aggregate['content_type'] == content_type:
                get_kwargs = {
                    'content_type' : content_type,
                    'unique_field' : smart_unicode(aggregate['unique_field']),
                    'unique_field_data' : smart_unicode(smart_getattr(instance, str(aggregate['unique_field']))),
                }
                object, created = Aggregate.objects.get_or_create(**get_kwargs)
                object.count = object.count + 1
                if 'callback' in aggregate:
                    agg_func = aggregate['callback']
                    object.user_defined = agg_func(
                        instance, 
                        object.user_defined, 
                        'increment'
                    )
                object.save()

def decrement_aggregate(sender, instance):
    """
    Called post_delete.  This will check for uniqueness and delete a new
    Aggregate object if needed.
    """
    content_type = ContentType.objects.get_for_model(sender)
    for aggregate in aggregates:
        if aggregate['content_type'] == content_type:
            get_kwargs = { 
                'content_type' : content_type,
                'unique_field' : smart_unicode(aggregate['unique_field']),
                'unique_field_data' : smart_unicode(smart_getattr(instance, str(aggregate['unique_field']))),
            }
            object, created = Aggregate.objects.get_or_create(**get_kwargs)
            object.count = object.count - 1
            # If it has no related objects, it cannot be useful, so delete it
            if object.count <= 0:
                object.delete()
            else:
                if 'callback' in aggregate:
                    agg_func = aggregate['callback']
                    object.user_defined = agg_func(
                        instance, 
                        object.user_defined, 
                        'decrement'
                    )
                object.save()

# The actual connections to the dispatcher are made here
dispatcher.connect(pre_save_annotation, pre_save)
dispatcher.connect(increment_aggregate, post_save)
dispatcher.connect(decrement_aggregate, post_delete)